<!DOCTYPE html>
<html>

	<head>
		<meta charset="UTF-8">
		<title></title>
		<style>
			canvas {
				border: 1px solid gray;
			}
			
			.btn-row {
				width: 400px;
				display: flex;
				justify-content: space-around;
			}
		</style>
	</head>

	<body>
		<canvas width="400"
		 height="400"
		 id='my-canvas'></canvas>

		<div class="btn-row">
			<button id='up-btn'>up</button>
			<button id='down-btn'>down</button>
			<button id='left-btn'>left</button>
			<button id='right-btn'>right</button>
			<button id='undo-btn'>undo</button>
			<button id='redo-btn'>redo</button>
		</div>
	</body>
	<script>
		const canvas = document.getElementById('my-canvas')
		const CanvasWidth = 400 // 画布宽度
		const CanvasHeight = 400 // 画布高度
		const CanvasStep = 40 // 动作步长
		canvas.width = CanvasWidth
		canvas.height = CanvasHeight

		const btnUp = document.getElementById('up-btn')
		const btnDown = document.getElementById('down-btn')
		const btnLeft = document.getElementById('left-btn')
		const btnRight = document.getElementById('right-btn')
		const btnUndo = document.getElementById('undo-btn')
		const btnRedo = document.getElementById('redo-btn')

		// 移动对象类
		class Role {
			constructor(x, y, imgSrc) {
				this.x = x
				this.y = y
				this.canvas = document.getElementById('my-canvas')
				this.ctx = this.canvas.getContext('2d')
				this.img = new Image()
				this.img.style.width = CanvasStep
				this.img.style.height = CanvasStep
				this.img.src = imgSrc
				this.img.onload = () => {
					this.ctx.drawImage(this.img, x, y, CanvasStep, CanvasStep)
					this.move(0, 0)
				}

				// 画一个蘑菇
				const mushroom = new Image()
				mushroom.style.width = CanvasStep
				mushroom.style.height = CanvasStep
				mushroom.src = 'https://i.loli.net/2019/08/12/amgWhQ5K3UHZy27.png'
				mushroom.onload = () => {
					this.ctx.drawImage(mushroom, CanvasWidth - CanvasStep, 0, CanvasStep, CanvasStep)
				}
			}

			move(x, y) {
				this.ctx.clearRect(this.x, this.y, CanvasStep, CanvasStep)
				this.x += x
				this.y += y
				this.ctx.drawImage(this.img, this.x, this.y, CanvasStep, CanvasStep)
				this.check()
			}

			check() {
				if(this.x === CanvasWidth - CanvasStep && this.y === 0) {
					setTimeout(() =>
						alert('挑战成功'), 200)
				}
			}
		}

		// 向上移动命令对象
		const MoveUpCommand = {
			execute(role) {
				role.move(0, -CanvasStep)
			},
			undo(role) {
				role.move(0, CanvasStep)
			}
		}

		// 向下移动命令对象
		const MoveDownCommand = {
			execute(role) {
				role.move(0, CanvasStep)
			},
			undo(role) {
				role.move(0, -CanvasStep)
			}
		}

		// 向左移动命令对象
		const MoveLeftCommand = {
			execute(role) {
				role.move(-CanvasStep, 0)
			},
			undo(role) {
				role.move(CanvasStep, 0)
			}
		}

		// 向右移动命令对象
		const MoveRightCommand = {
			execute(role) {
				role.move(CanvasStep, 0)
			},
			undo(role) {
				role.move(-CanvasStep, 0)
			}
		}

		// 命令管理者
		const CommandManager = {
			undoStack: [], // 撤销命令栈
			redoStack: [], // 重做命令栈

			executeCommand(role, command) {
				this.redoStack.length = 0 // 每次执行清空重做命令栈
				this.undoStack.push(command) // 推入撤销命令栈
				command.execute(role)
			},

			/* 撤销 */
			undo(role) {
				if(this.undoStack.length === 0) return
				const lastCommand = this.undoStack.pop()
				lastCommand.undo(role)
				this.redoStack.push(lastCommand) // 放入redo栈中
			},

			/* 重做 */
			redo(role) {
				if(this.redoStack.length === 0) return
				const lastCommand = this.redoStack.pop()
				lastCommand.execute(role)
				this.undoStack.push(lastCommand) // 放入undo栈中
			}
		}

		// 设置按钮命令
		const setCommand = function(element, role, command) {
			if(typeof command === 'object') {
				element.onclick = function() {
					CommandManager.executeCommand(role, command)
				}
			} else {
				element.onclick = function() {
					command.call(CommandManager, role)
				}
			}
		}

		/* ----- 客户端 ----- */
		const mary = new Role(200, 200, 'https://i.loli.net/2019/08/09/sqnjmxSZBdPfNtb.jpg')

		setCommand(btnUp, mary, MoveUpCommand)
		setCommand(btnDown, mary, MoveDownCommand)
		setCommand(btnLeft, mary, MoveLeftCommand)
		setCommand(btnRight, mary, MoveRightCommand)

		setCommand(btnUndo, mary, CommandManager.undo)
		setCommand(btnRedo, mary, CommandManager.redo)
	</script>

</html>